package com.jy.appmarket.download;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.concurrent.atomic.AtomicBoolean;

import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;

import com.jy.appmarket.utils.LogUtil;

/**
 * 类说明：
 * 
 * @author Baker.li
 * @date 2014年5月19日
 * @version 1.0
 */

public class DownloadThread extends Thread {
	private static final String TAG = "DownloadThread";
	private final static int TIME_OUT = 30 * 1000;

	public interface DownloadThreadListener {
		void OnDownloadConnected(String id, long bytes);

		void onDownloadFinished(String id);

		void OnUpdateProgress(String id, long downloadedBytes, long speed);

		void OnPausedByUser(String id);

		void onPausedByError(String id, long detail);
	}

	private DownloadThreadListener mListener;

	/** error codes */
	private static final int EC_URL_ERROR = -1;
	private static final int EC_CONNECTION_FAILED = -2;
	private static final int EC_SERVER_CONN_ERROR = -3;
	private static final int EC_SERVER_CODE_ERROR = -4;
	private static final int EC_SERVER_CONTENT_LEN_ERROR = -5;
	private static final int EC_FILE_ERROR = -6;
	private static final int EC_UNKNOWN_ERROR = -7;
	// private static final int EC_SAVE_BODY_IO_ERROR = -8;
	private static final int EC_REDIRECT_ERROR = -9;

	private static final int BUFFER_SIZE = 1024 * 100;// 50K
	private static final int MAX_REDIRECT = 5;

	private String mTaskId;
	private String mTmpFilePath;
	private String mDestFilePath;
	private long mDownLoadedBytes;
	private boolean mIsDownloading;
	private AtomicBoolean mShouldStop = new AtomicBoolean();
	private String mDownloadUrl;
	private Context mContext;

	/**
	 * 处理302跳转的时候遇到最终下载的文件已经更新的情况，需要用以前的文件大小进行判断 如果文件大小不同，需要把之前的tmp文件删除，从头开始重新下载
	 */
	private long mFileSize;

	private static Handler sHandler = new Handler(Looper.getMainLooper());

	public DownloadThread(Context context, String taskId, String dlUrl, DownloadThreadListener l,
			String destPath, long fileSize) {
		mContext = context;
		mDownloadUrl = dlUrl;
		mListener = l;
		mTaskId = taskId;
		mDestFilePath = destPath;
		mFileSize = fileSize;
		mTmpFilePath = mDestFilePath + ".dl";
	}

	public void setDownloadListener(DownloadThreadListener listener) {
		mListener = listener;
	}

	public void stopDownload() {
		mShouldStop.set(true);
	}

	public boolean shouldStop() {
		return mShouldStop.get();
	}

	public boolean isDownloading() {
		return mIsDownloading;
	}

	private boolean checkEnvironment() {
		File parentDir = new File(mDestFilePath).getParentFile();
		if (!parentDir.exists()) {
			boolean ret = parentDir.mkdirs();
			if (!ret) {
				Log.e(TAG, "init failed");
			}
			return ret;
		}
		return true;
	}

	private void initParams() {
		mTmpFilePath = mDestFilePath + ".dl";

		File fh = new File(mTmpFilePath);
		if (fh.exists()) {
			mDownLoadedBytes = fh.length();
		} else {
			mDownLoadedBytes = 0;
		}
		mIsDownloading = true;
		mShouldStop.set(false);
	}

	@Override
	public void run() {
		// steps:
		// 1.check params, init environment
		// 2.connect
		// 3.get header
		// 4.read content
		// 5.finalize
		if (!checkEnvironment()) {
			sendCallback(MSG_INIT_FAILED, 0, 0);
			mIsDownloading = false;
			return;
		}

		initParams();
		boolean errorOccured = false;
		HttpURLConnection conn = null;
		try {
			conn = getDestConnection(mDownloadUrl);
		} catch (IOException e) {
			mIsDownloading = false;
			errorOccured = true;
		}

		if (errorOccured) {
			mIsDownloading = false;
			sendCallback(MSG_PAUSED_ERROR, EC_REDIRECT_ERROR, 0);
			return;
		}

		try {
			URL url = new URL(mDownloadUrl);
			conn = (HttpURLConnection) url.openConnection();
		} catch (IOException e) {
			errorOccured = true;
		}
		if (errorOccured) {
			mIsDownloading = false;
			sendCallback(MSG_PAUSED_ERROR, EC_URL_ERROR, 0);
			return;
		}

		try {
			conn.setConnectTimeout(TIME_OUT);
			conn.setReadTimeout(TIME_OUT);
			conn.setRequestMethod("GET");
			// set default headers
			if (mDownLoadedBytes > 0) { // 支持断点续传
				conn.setRequestProperty("Range", "bytes=" + mDownLoadedBytes + "-");
			}
			conn.connect();
		} catch (IOException e) {
			e.printStackTrace();
			errorOccured = true;
		}

		if (errorOccured) {
			mIsDownloading = false;
			sendCallback(MSG_PAUSED_ERROR, EC_CONNECTION_FAILED, 0);
			return;
		}

		if (mShouldStop.get()) {
			conn.disconnect();
			mIsDownloading = false;
			sendCallback(MSG_PAUSED_USER, 0, 0);
			return;
		}

		int statusCode = -1;
		try {
			statusCode = conn.getResponseCode();
		} catch (IOException e) {
			e.printStackTrace();
			errorOccured = true;
		}
		if (errorOccured) {
			conn.disconnect();
			mIsDownloading = false;
			sendCallback(MSG_PAUSED_ERROR, EC_SERVER_CODE_ERROR, 0);
			return;
		}

		if (statusCode != 200 && statusCode != 206) {
			Log.e(TAG, "http status code:" + statusCode);
			conn.disconnect();
			// 所请求的范围无法满足,需要删除临时文件重新下载
			if (statusCode == 416) {
				File tmp = new File(mTmpFilePath);
				tmp.delete();
			}
			mIsDownloading = false;
			sendCallback(MSG_PAUSED_ERROR, EC_SERVER_CODE_ERROR, 0);
			return;
		}

		if (statusCode == 200) {// 有可能服务器不支持断点续传，则需要从头开始下载
			mDownLoadedBytes = 0;
			File tmp = new File(mTmpFilePath);
			tmp.delete();
		}

		long contentLen = conn.getContentLength();
		if (contentLen <= 0) {
			conn.disconnect();
			mIsDownloading = false;
			sendCallback(MSG_PAUSED_ERROR, EC_SERVER_CONTENT_LEN_ERROR, 0);
			return;
		}
		sendCallback(MSG_CONNECTED, Long.valueOf(contentLen + mDownLoadedBytes), 0);

		if (mShouldStop.get()) {
			conn.disconnect();
			mIsDownloading = false;
			sendCallback(MSG_PAUSED_USER, 0, 0);
			return;
		}

		InputStream is = null;
		RandomAccessFile rafileHandle = null;
		int readed = 0;
		try {
			try {
				rafileHandle = new RandomAccessFile(new File(mTmpFilePath), "rw");
			} catch (FileNotFoundException e) {
				e.printStackTrace();
				errorOccured = true;
			}
			if (errorOccured) {
				mIsDownloading = false;
				sendCallback(MSG_PAUSED_ERROR, EC_FILE_ERROR, 0);
			}

			try {
				is = conn.getInputStream();
			} catch (IOException e) {
				e.printStackTrace();
				errorOccured = true;
			}

			if (errorOccured) {
				mIsDownloading = false;
				sendCallback(MSG_PAUSED_ERROR, EC_SERVER_CONN_ERROR, 0);
				return;
			}

			long now = System.currentTimeMillis();
			byte[] buffer = new byte[BUFFER_SIZE];
			boolean netOrFileError = true;

			long downloadedSizeThisTime = 0;
			long timebg = now;
			
			while (!mShouldStop.get()) {
				try {
					readed = is.read(buffer, 0, BUFFER_SIZE);
				} catch (IOException e) {
					e.printStackTrace();
					errorOccured = true;
					netOrFileError = true;
					break;
				}

				if (readed == -1) {// end of stream
					break;
				}
				
				downloadedSizeThisTime += readed;

				try {
					rafileHandle.seek(mDownLoadedBytes);
					rafileHandle.write(buffer, 0, readed);
				} catch (IOException e) {
					e.printStackTrace();
					errorOccured = true;
					netOrFileError = false;
					break;
				}
				mDownLoadedBytes += readed;
				
				if (System.currentTimeMillis() - now > 1000) {
					now = System.currentTimeMillis();

					long timeEnd = System.currentTimeMillis();
					long timeBtw = (timeEnd - timebg) / 1000;
					if(timeBtw != 0){
						sendCallback(MSG_PROGRESS, mDownLoadedBytes, downloadedSizeThisTime / timeBtw);
					}
				}
			}

			if (errorOccured) {
				mIsDownloading = false;
				if (netOrFileError) {
					sendCallback(MSG_PAUSED_ERROR, EC_SERVER_CONN_ERROR, 0);
				} else {
					sendCallback(MSG_PAUSED_ERROR, EC_FILE_ERROR, 0);
				}
				return;
			}

			if (mShouldStop.get()) {
				mIsDownloading = false;
				sendCallback(MSG_PAUSED_USER, 0, 0);
				return;
			}
		} finally {
			try {
				if (rafileHandle != null) {
					rafileHandle.close();
				}
				if (is != null) {
					is.close();
				}
				conn.disconnect();
			} catch (Exception e) {
				e.printStackTrace();
			}
		}

		if (errorOccured) {
			mIsDownloading = false;
			sendCallback(MSG_PAUSED_ERROR, EC_UNKNOWN_ERROR, 0);
			return;
		}

		File save = new File(mDestFilePath);
		File tmp = new File(mTmpFilePath);

		if (!save.exists()) {// 把临时文件名重命名为正式文件名
			tmp.renameTo(save);
		} else {
			save.delete();
			tmp.renameTo(save);
		}
		mIsDownloading = false;
		sendCallback(MSG_FINISHED, 0, 0);
	}

	private HttpURLConnection getDestConnection(String urlString) throws IOException {
		URL url = null;
		HttpURLConnection conn = null;
		String referer = null;
		int count = 0;
		while (count++ < MAX_REDIRECT) {
			url = new URL(urlString);
			if (DownloadUtils.isCmwap(mContext)) {
				conn = DownloadUtils.getCmwapConnect(urlString);
			} else {
				conn = (HttpURLConnection) url.openConnection();
			}
			conn.setInstanceFollowRedirects(false);
			conn.setConnectTimeout(TIME_OUT);
			conn.setReadTimeout(TIME_OUT);
			conn.setRequestMethod("GET");
			DownloadUtils.setCommonHttpHeader(conn);
			if (mDownLoadedBytes > 0) {
				conn.setRequestProperty("Range", "bytes=" + mDownLoadedBytes + "-");
			}

			if (referer != null && referer.length() != 0) {
				conn.setRequestProperty("Referer", referer);
			}

			if (conn.getResponseCode() == 302 || conn.getResponseCode() == 301) {
				urlString = conn.getHeaderField("Location");
				referer = conn.getHeaderField("Referer");
//				LogUtil.d(TAG, "redirect:" + urlString);
			} else {
				if (conn.getResponseCode() == 206 && mFileSize != 0
						&& mDownLoadedBytes + conn.getContentLength() != mFileSize) {
					Log.w(TAG, "file size unmatched.");
					File tmp = new File(mTmpFilePath);
					tmp.delete();
					mDownLoadedBytes = 0;
					continue;
				}
				return conn;
			}
		}
		LogUtil.w(TAG, "too many redirect");
		return null;
	}

	private void sendCallback(int msg, long param1, long param2) {
		CallbackRunnable r = new CallbackRunnable(mListener, mTaskId, msg, param1, param2);
		sHandler.post(r);
	}

	private static final int MSG_INIT_FAILED = 1;
	private static final int MSG_CONNECTED = 2;
	private static final int MSG_FINISHED = 3;
	private static final int MSG_PROGRESS = 4;
	private static final int MSG_PAUSED_USER = 5;
	private static final int MSG_PAUSED_ERROR = 6;

	private static class CallbackRunnable implements Runnable {
		private String mId;
		private int mMsg;
		private DownloadThreadListener mListener;
		private long mParam1;
		private long mParam2;

		public CallbackRunnable(DownloadThreadListener l, String id, int msg, long param1,
				long param2) {
			mId = id;
			mMsg = msg;
			mListener = l;
			mParam1 = param1;
			mParam2 = param2;
		}

		@Override
		public void run() {
			if (mListener == null) {
				Log.d(TAG, "listener is null");
				return;
			}
			switch (mMsg) {
			case MSG_INIT_FAILED:
				mListener.onPausedByError(mId, mParam1);
				break;
			case MSG_CONNECTED:
				mListener.OnDownloadConnected(mId, mParam1);
				break;
			case MSG_FINISHED:
				mListener.onDownloadFinished(mId);
				break;
			case MSG_PAUSED_ERROR:
				mListener.onPausedByError(mId, mParam1);
				break;
			case MSG_PAUSED_USER:
				mListener.OnPausedByUser(mId);
				break;
			case MSG_PROGRESS:
				mListener.OnUpdateProgress(mId, mParam1, mParam2);
				break;

			default:
				Log.w(TAG, "unknown msg");
				break;
			}
		}

	}

}
